"""
Copyright © 2023 Walkline Wang (https://walkline.wang)
Gitee: https://gitee.com/walkline/micropython-ws2812-led-clock
"""
__version__ = '0.1.4'
__version_info__ = (0, 1, 4)
print('module animation version:', __version__)

import time
from neopixel import NeoPixel

try:
	from utils.dispatcher import Dispatcher
except ImportError:
	from dispatcher import Dispatcher

try:
	from utils.utilities import Utilities
except ImportError:
	Utilities = __import__('utils/utilities').Utilities


CONFIG = Utilities.import_config()


class AnimationException(Exception):
	pass


class Animation(object):
	'''生成简易动画数据'''
	CONFIG_WIFI  = 0 # 配网指示
	CONNECT_WIFI = 1 # 联网指示
	HEARTBEAT    = 2 # 心跳动画
	UPDATING     = 3 # 在线更新动画
	SUCCESS      = 4 # 成功提示动画
	FAILED       = 5 # 失败提示动画
	BOOM         = 6 # 人机互动动画

	MENU_CLOCK      = 20 # 时钟菜单
	MENU_CALENDAR_1 = 21 # 台历1菜单
	MENU_CALENDAR_2 = 22 # 台历2菜单
	MENU_UPDATE     = 23 # 在线更新菜单

	DEFAULT_PERIOD = 50
	DEFAULT_STEPS  = 10
	DEFAULT_LOOPS  = True

	ANIMATION_LIST = [
		CONFIG_WIFI,
		CONNECT_WIFI,
		HEARTBEAT,
		MENU_CLOCK,
		MENU_CALENDAR_1,
		MENU_CALENDAR_2,
		MENU_UPDATE,
		UPDATING,
		SUCCESS,
		FAILED,
		BOOM,
	]

	RESOURCES = {
		# 呼吸效果的配网指示动画
		CONFIG_WIFI: {
			'period': 60,
			'steps' : 20,
			'frames': {
				0: 0x00080180380780,
			},
		},
		# 动态增加效果的连接指示动画
		CONNECT_WIFI: {
			'period': 200,
			'frames': {
				0: 0x00000000000000,
				1: 0x00080000000000,
				2: 0x00080180000000,
				3: 0x00080180380000,
				4: 0x00080180380780,
			}
		},
		# 心跳动画
		HEARTBEAT: {
			'period': 300,
			'frames': {
				0: 0x0000850a508000,
				1: 0x0000c48948c000,
				2: 0x08522448462508,
			}
		},
		# 在线更新动画
		UPDATING: {
			'period': 100,
			'frames': {
				 0: 0x00000001000000,
				 1: 0x00000042040000,
				 2: 0x00001084081000,
				 3: 0x00002108102000,
				 4: 0x00004211204000,
				 5: 0x00008462448000,
				 6: 0x00011884891000,
				 7: 0x00022108122000,
				 8: 0x00004210204000,
				 9: 0x00008420408000,
				10: 0x00010800810000,
				11: 0x00020000020000,
				12: 0x00000000000000,
			}
		},
		# 成功提示动画
		SUCCESS: {
			'period': 80,
			'loops' : False,
			'frames': {
				0: 0x00000000000000,
				1: 0x00100000000000,
				2: 0x00102000000000,
				3: 0x00102040000000,
				4: 0x00102042000000,
				5: 0x00102042100000,
				6: 0x00102042108000,
				7: 0x00102042108400,
				8: 0x00102042108400,
				9: 0x00102042108400,
			}
		},
		# 失败提示动画
		FAILED: {
			'period': 80,
			'loops' : False,
			'frames': {
				 0: 0x00000000000000,
				 1: 0x00000000010000,
				 2: 0x00000000210000,
				 3: 0x00000004210000,
				 4: 0x00000084210000,
				 5: 0x00011084210000,
				 6: 0x00011284210000,
				 7: 0x00011284290000,
				 8: 0x00011284291000,
				 9: 0x00011284291000,
				10: 0x00011284291000,
			}
		},
		# 时钟菜单图标
		MENU_CLOCK: {
			'period': 40,
			'loops' : False,
			'frames': {
				 0: 0x00000000000000,
				 1: 0x0c000000000000,
				 2: 0x12300000000000,
				 3: 0x2548c000000000,
				 4: 0x2d952300000000,
				 5: 0x21b6548c000000,
				 6: 0x1286d952300000,
				 7: 0x0c4a1b6548c000,
				 8: 0x0031286d952300,
				 9: 0x0031286d952300,
				10: 0x0031286d952300,
			}
		},
		# 台历1菜单图标
		MENU_CALENDAR_1: {
			'period': 40,
			'loops' : False,
			'frames': {
				 0: 0x00000000000000,
				 1: 0x1f000000000000,
				 2: 0x217c0000000000,
				 3: 0x2385f000000000,
				 4: 0x2f8e17c0000000,
				 5: 0x2bbe385f000000,
				 6: 0x21aef8e17c0000,
				 7: 0x1f86bbe385f000,
				 8: 0x007e1aef8e17c0,
				 9: 0x007e1aef8e17c0,
				10: 0x007e1aef8e17c0,
			}
		},
		# 台历2菜单图标
		MENU_CALENDAR_2: {
			'period': 40,
			'loops' : False,
			'frames': {
				 0: 0x00000000000000,
				 1: 0x1f000000000000,
				 2: 0x217c0000000000,
				 3: 0x2385f000000000,
				 4: 0x2f8e17c0000000,
				 5: 0x29be385f000000,
				 6: 0x21a6f8e17c0000,
				 7: 0x1f869be385f000,
				 8: 0x007e1a6f8e17c0,
				 9: 0x007e1a6f8e17c0,
				10: 0x007e1a6f8e17c0,
			}
		},
		# 在线更新菜单图标
		MENU_UPDATE: {
			'period': 40,
			'loops' : False,
			'frames': {
				0: 0x00000000000000,
				1: 0x08000000000000,
				2: 0x10200000000000,
				3: 0x2f408000000000,
				4: 0x10bd0200000000,
				5: 0x0842f408000000,
				6: 0x00210bd0200000,
				7: 0x0000842f408000,
				8: 0x0000842f408000,
				9: 0x0000842f408000,
			}
		},
		# 人机互动动画
		BOOM: {
			'period': 140,
			'loops': False,
			'frames': {
				 0: 0x000077df7c7000,
				 1: 0x000073cf3c7000,
				 2: 0x000071c71c7000,
				 3: 0x0000040e400000,
				 4: 0x00000207200000,
				 5: 0x00000103100000,
				 6: 0x1f04706304304f,
				 7: 0x1f04705304304f,
				 8: 0x1f04704b04304f,
				 9: 0x1f04725724304f,
				10: 0x1f04f56354b04f,
				11: 0x1f2578c18d724f,
				12: 0x1f56304006354f,
				13: 0x178c10000018d7,
				14: 0x23040000000063,
				15: 0x01000000000001,
				16: 0x00000000000000,
			},
		},
	}

	def __init__(self, neopixel:NeoPixel, tasks:Dispatcher):
		self.__neopixel = neopixel
		self.__tasks    = tasks

		self.__period = Animation.DEFAULT_PERIOD
		self.__loops  = Animation.DEFAULT_LOOPS
		self.__steps  = Animation.DEFAULT_STEPS
		self.__frames = None
		self.__colors = None

		self.__normal_direction = True

		self.__frames_gen = None
		self.__colors_gen = None

		self.__task_show_animation = lambda: self.__show_animation_cb()

		self.__get_index = lambda x: x if self.__normal_direction else (53-x)

	def __select_animation(self, animation:int, colors:tuple):
		'''选择一个动画效果'''
		if animation not in Animation.ANIMATION_LIST:
			raise AnimationException('invalid animation')

		resource = Animation.RESOURCES[animation]
		frames = resource.get('frames')

		if frames and len(frames) > 0:
			self.__frames = frames
			self.__frames_gen = self.__frame_generator()
		else:
			raise AnimationException('invalid frames')

		if isinstance(colors, (tuple, list)) and len(colors) > 0:
			self.__colors = colors
			self.__colors_gen = self.__color_generator()
		else:
			raise AnimationException('invalid colors')

		self.__period = resource.get('period', Animation.DEFAULT_PERIOD)
		self.__loops  = resource.get('loops', Animation.DEFAULT_LOOPS)
		self.__steps  = resource.get('steps', Animation.DEFAULT_STEPS)

	def __get_frame_and_color(self) -> tuple:
		'''
		获取当前帧数据和颜色

		@return: 剩余帧数, 当前帧数据, 当前颜色数据
		'''
		remains, frame = next(self.__frames_gen)
		return remains, frame, next(self.__colors_gen)

	def __frame_generator(self) -> tuple:
		'''帧数据生成器'''
		index = 0
		count = len(self.__frames)

		while True:
			yield count - index - 1, self.__frames[index]
			index = (index + 1) % count

	def __color_generator(self) -> tuple:
		'''颜色数据生成器'''
		while True:
			if isinstance(self.colors[0], int):
				yield self.colors
			elif isinstance(self.colors[0], tuple):
				if len(self.colors) == 1:
					yield self.colors[0]
				else:
					for index, color in enumerate(self.colors):
						next_index = (index + 1) % len(self.colors)
						next_color = self.colors[next_index]

						for step in range(1, self.steps + 1):
							r = int(color[0] + (next_color[0] - color[0]) * step / self.steps)
							g = int(color[1] + (next_color[1] - color[1]) * step / self.steps)
							b = int(color[2] + (next_color[2] - color[2]) * step / self.steps)

							yield (r, g, b)

	def stop(self):
		self.__tasks.del_work(self.__task_show_animation)

	def show(self, animation:int, colors:tuple):
		'''异步播放简易动画'''
		if animation not in Animation.ANIMATION_LIST:
			return

		if not isinstance(colors, (tuple, list)):
			return

		self.__tasks.del_work(self.__task_show_animation)
		self.__select_animation(animation, colors)
		# self.__show_animation_cb()
		self.__tasks.add_work(self.__task_show_animation, self.period)

	def show_sync(self, animation:int, colors:tuple):
		'''同步播放简易动画'''
		if animation not in Animation.ANIMATION_LIST:
			return

		if not isinstance(colors, (tuple, list)):
			return

		self.__select_animation(animation, colors)

		for _ in range(len(self.__frames)):
			self.__show_animation_cb()
			time.sleep_ms(self.period)

	def is_normal_direction(self, value:bool):
		self.__normal_direction = value

	def __show_animation_cb(self):
		'''显示动画回调函数'''
		remains, frame, color = self.__get_frame_and_color()

		for index, bit in enumerate(self.__zfill_54bin(frame)):
			self.__neopixel[self.__get_index(index)] = color if bit == '1' else CONFIG.COLORS.BLACK

		self.__neopixel.write()

		if not self.loops and remains == 0:
			self.__tasks.del_work(self.__task_show_animation)

	def __zfill_54bin(self, value:int):
		'''将整型转为 54 位二进制字符串'''
		return f'{value:0>54b}'


	#region class properties
	@property
	def steps(self):
		return self.__steps

	@steps.setter
	def steps(self, value:int):
		'''设置/获取颜色分段数量'''
		self.__steps = value

	@property
	def period(self):
		return self.__period

	@period.setter
	def period(self, value:int):
		'''设置/获取显示间隔时长'''
		self.__period = value

	@property
	def loops(self):
		return self.__loops

	@loops.setter
	def loops(self, value:bool):
		'''设置/获取是否循环播放'''
		self.__loops = value

	@property
	def colors(self):
		return self.__colors

	@colors.setter
	def colors(self, value:tuple(tuple, list)):
		'''设置/获取要显示的颜色'''
		if isinstance(value, (tuple, list)) and len(value) > 0:
			self.__colors = value
			self.__colors_gen = self.__color_generator(value)
	#endregion


	#region dummy
	_ = 0
	#endregion dummy


if __name__ == '__main__':
	from utils.dispatcher import Dispatcher
	from matrix.ws2812 import WS2812
	from utils.utilities import Utilities


	CONFIG = Utilities.import_config()
	current_animation = 0

	def show_tests():
		global current_animation
		colors = None

		resource_index = Animation.ANIMATION_LIST[current_animation]

		if resource_index == Animation.CONFIG_WIFI:
			colors = (
				ws2812.convert_color((255, 0, 0)),
				ws2812.convert_color((0, 255, 0)),
				ws2812.convert_color((0, 0, 255))
			)
		elif resource_index == Animation.CONNECT_WIFI:
			colors = ws2812.convert_color((0, 255, 0))
		elif resource_index == Animation.HEARTBEAT:
			colors = ws2812.convert_color((255, 0, 0))
		elif resource_index == Animation.MENU_CLOCK:
			colors = ws2812.convert_color((128, 128, 128))
		elif resource_index == Animation.MENU_CALENDAR_1:
			colors = ws2812.convert_color((128, 0, 128))
		elif resource_index == Animation.MENU_CALENDAR_2:
			colors = ws2812.convert_color((0, 128, 128))
		elif resource_index == Animation.MENU_UPDATE:
			colors = ws2812.convert_color((0, 128, 128))
		elif resource_index == Animation.UPDATING:
			colors = ws2812.convert_color((128, 128, 128))
		elif resource_index == Animation.SUCCESS:
			colors = ws2812.convert_color((0, 128, 0))
		elif resource_index == Animation.FAILED:
			colors = ws2812.convert_color((128, 0, 0))
		elif resource_index == Animation.BOOM:
			colors = ws2812.convert_color((127, 127, 127))

		animation.show(resource_index, colors)
		tasks.add_work(show_tests, 8000)

		current_animation = (current_animation + 1) % len(Animation.ANIMATION_LIST)


	ws2812    = WS2812(CONFIG.WS2812_MATRIX.WIDTH, CONFIG.WS2812_MATRIX.HEIGHT, CONFIG.PINS.DIN_MATRIX)
	tasks     = Dispatcher()
	animation = Animation(ws2812.__neopixel, tasks)

	ws2812.brightness = 40

	show_tests()
